/*
* Sun Public License Notice
*
* The contents of this file are subject to the Sun Public License
* Version 1.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://www.sun.com/
*
* The Original Code is Forte for Java, Community Edition. The Initial
* Developer of the Original Code is Sun Microsystems, Inc. Portions
* Copyright 1997-2000 Sun Microsystems, Inc. All Rights Reserved.
*/
package org.netbeans.modules.emacs;
import java.awt.Color;
import java.awt.Rectangle;
import java.beans.*;
import java.util.*;
import javax.swing.SwingUtilities;
import javax.swing.text.*;
import javax.swing.undo.*;
import org.openide.text.NbDocument;
import org.openide.loaders.DataObject;
import org.openide.util.WeakListener;
import org.openide.cookies.CloseCookie;
public class EmacsDocument extends DefaultStyledDocument implements Protocol, NbDocument.WriteLockable {
private static final Object GUARDED = NbDocument.GUARDED;
private static final MutableAttributeSet GUARDED_LOOK = new SimpleAttributeSet ();
static {
GUARDED_LOOK.addAttribute (StyleConstants.ColorConstants.Background, Color.cyan);
}
private static final MutableAttributeSet UNGUARDED_LOOK = new SimpleAttributeSet ();
static {
// XXX is it possible to simply disable the color somehow?
UNGUARDED_LOOK.addAttribute (StyleConstants.ColorConstants.Background, Color.white);
}
public EmacsDocument () {
setDocumentProperties (new HookProperties (getDocumentProperties ()));
}
private transient EmacsProxier proxy = null;
private transient EmacsListener elistener = null;
/**
* @associates Object
*/
private transient Vector pendingGuards = null;
/**
* @associates Object
*/
private transient Vector pendingStyles = null;
private transient PropertyChangeListener dobPCL = new PropertyChangeListener () {
public void propertyChange (PropertyChangeEvent ev) {
if (DataObject.PROP_MODIFIED.equals (ev.getPropertyName ())) {
if (proxy == null) {
if (Connection.DEBUG) System.err.println("skipping setModified since proxy == null");
return;
}
proxy.call (CMD_setModified, new Object[] { ev.getNewValue () });
}
}
};
private synchronized void addGuard (int off, int len, boolean guard) {
if (Connection.DEBUG) System.err.println("EmacsDocument.addGuard: off=" + off + " len=" + len + " guard=" + guard);
if (pendingGuards == null) pendingGuards = new Vector ();
try {
if (Connection.DEBUG) System.err.println("Guardable text: " + getText (off, len));
Position start = NbDocument.createPosition (this, off, Position.Bias.Forward);
Position end = NbDocument.createPosition (this, off + len, Position.Bias.Backward);
pendingGuards.add (new Object[] { start, end, new Boolean (guard) });
} catch (BadLocationException ble) {
ble.printStackTrace ();
}
}
private synchronized void addStyle (int off, String type) {
if (Connection.DEBUG) System.err.println("EmacsDocument.addStyle: off=" + off + " type=" + type);
if (pendingStyles == null) pendingStyles = new Vector ();
try {
if (Connection.DEBUG) {
Element el = getParagraphElement (off);
System.err.println("Styled text: " + getText (el.getStartOffset (), el.getEndOffset () - el.getStartOffset ()));
}
Position pos = NbDocument.createPosition (this, off, Position.Bias.Forward);
pendingStyles.add (new Object[] { pos, type });
} catch (BadLocationException ble) {
ble.printStackTrace ();
}
}
synchronized void init (final EmacsProxier proxy) {
if (proxy == this.proxy) return;
if (this.proxy != null) throw new IllegalArgumentException ();
if (atomicLevel > 0) proxy.call (CMD_startAtomic);
if (asUserLevel > 0) proxy.call (CMD_setAsUser, new Object[] { Boolean.TRUE });
if (Connection.DEBUG) System.err.println ("Initing EmacsDocument with proxy " + proxy);
this.proxy = proxy;
proxy.addEmacsListener (elistener = new EmacsListener () {
public void callback (EmacsEvent ev) {
handleCallback (ev);
}
});
// Assume we are always listening:
proxy.call (CMD_startDocumentListen);
maybeSetTitle (getProperty (TitleProperty));
Object dob = getProperty (StreamDescriptionProperty);
if (dob != null && dob instanceof DataObject)
proxy.call (CMD_setModified, new Object[] { new Boolean (((DataObject) dob).isModified ()) });
render (new Runnable () {
public void run () {
int len = ((Integer) proxy.function (FUN_getLength)[0]).intValue ();
if (len != 0) {
// XXX check result
proxy.function (FUN_remove, new Object[] { new Integer (0), new Integer (len) });
}
len = getLength ();
if (len != 0) {
try {
String text = getText (0, len);
insertInChunks (0, text);
} catch (BadLocationException ble) {
ble.printStackTrace ();
markAsBogus ("Could not initialize content: " + ble);
return;
}
if (pendingGuards != null) {
Iterator it = pendingGuards.iterator ();
while (it.hasNext ()) {
Object[] guard = (Object[]) it.next ();
int start = ((Position) guard[0]).getOffset ();
int end = ((Position) guard[1]).getOffset ();
if (end > start) {
proxy.call (((Boolean) guard[2]).booleanValue () ? CMD_guard : CMD_unguard,
new Object[] { new Integer (start), new Integer (end - start) });
} else {
if (Connection.DEBUG) System.err.println("ignoring empty/backwards guard: start=" + start + " end=" + end);
}
}
pendingGuards = null;
}
if (pendingStyles != null) {
Iterator it = pendingStyles.iterator ();
while (it.hasNext ()) {
Object[] style = (Object[]) it.next ();
proxy.call (CMD_setStyle, new Object[] { new Integer (((Position) style[0]).getOffset ()),
style[1] });
}
pendingStyles = null;
}
}
}
});
}
synchronized void shutdown () {
if (proxy == null) return;
if (Connection.DEBUG) System.err.println ("EmacsDocument dispose");
proxy.call (CMD_stopDocumentListen);
proxy.removeEmacsListener (elistener);
// don't bother turning off asUser, irrelevant
if (atomicLevel > 0) proxy.call (CMD_endAtomic);
pendingGuards = null;
proxy = null;
elistener = null;
}
protected void finalize () throws Exception {
shutdown ();
}
protected void markAsBogus (String msg) {
System.err.println("*** Document is bogus! ***");
System.err.println("Reason: " + msg);
shutdown ();
// XXX may want to do something like:
// init (oldProxy);
// (this is local-is-preferred mode)
}
protected void handleCallback (EmacsEvent ev) {
if (ev.isOutOfSequence ()) {
if (Connection.DEBUG) System.err.println("Will ignore bad event: " + ev);
markAsBogus ("Bad event: " + ev);
return;
}
if (EVT_insert.equals (ev.getType ())) {
Object[] args = ev.getArgs ();
if (args.length != 2) throw new IllegalArgumentException (ev.toString ());
int pos = ((Integer) args[0]).intValue ();
String text = (String) args[1];
if (text.equals ("")) return;
handleInsertEvent (pos, text);
} else if (EVT_remove.equals (ev.getType ())) {
Object[] args = ev.getArgs ();
if (args.length != 2) throw new IllegalArgumentException (ev.toString ());
int pos = ((Integer) args[0]).intValue ();
int len = ((Integer) args[1]).intValue ();
if (len == 0) return;
handleRemoveEvent (pos, len);
} else if (EVT_killed.equals (ev.getType ())) {
if (ev.getArgs ().length != 0) throw new IllegalArgumentException (ev.toString ());
handleKilledEvent ();
}
}
private transient boolean nofire = false;
private transient final Object nofireLock = new Object () {
public String toString () {
return "EmacsDocument.nofireLock";
}
};
// Runs runnable without firing changes to Emacs.
protected final void withoutFiring (Runnable run) {
synchronized (nofireLock) {
if (nofire) throw new IllegalStateException ();
try {
nofire = true;
run.run ();
} finally {
nofire = false;
}
}
}
protected void handleInsertEvent (final int pos, final String text) {
withoutFiring (new Runnable () {
public void run () {
try {
insertString (pos, text, SimpleAttributeSet.EMPTY);
} catch (BadLocationException ble) {
ble.printStackTrace ();
markAsBogus (ble.toString ());
}
}
});
}
protected void handleRemoveEvent (final int pos, final int len) {
withoutFiring (new Runnable () {
public void run () {
try {
remove (pos, len);
} catch (BadLocationException ble) {
ble.printStackTrace ();
markAsBogus (ble.toString ());
}
}
});
}
protected void handleKilledEvent () {
SwingUtilities.invokeLater (new Runnable () {
public void run () {
shutdown ();
Object dob = getProperty (StreamDescriptionProperty);
if (dob != null && dob instanceof DataObject) {
CloseCookie cookie = (CloseCookie) ((DataObject) dob).getCookie (CloseCookie.class);
if (cookie != null) cookie.close ();
}
}
});
}
private transient int atomicLevel = 0;
private transient int asUserLevel = 0;
private transient BadLocationException asUserException = null;
private transient Thread atomicThread = null;
protected synchronized void writeLock2 () {
try {
while (atomicThread != null && atomicThread != Thread.currentThread ())
wait ();
if (atomicLevel++ == 0) atomicThread = Thread.currentThread ();
} catch (InterruptedException ie) {
ie.printStackTrace ();
}
}
protected synchronized void writeUnlock2 () {
if (--atomicLevel == 0) atomicThread = null;
notifyAll ();
}
protected boolean testModification (int off) {
Element elt = getCharacterElement (off);
if (elt == null) return true;
return ! isGuarded (elt.getAttributes ());
}
public void insertString (int off, String str, AttributeSet attr) throws BadLocationException {
if (Connection.DEBUG) System.err.println("insertString: off=" + off + " str=" + str + " attr=" + attr);
//if (Connection.DEBUG) Thread.dumpStack ();
try {
writeLock2 ();
boolean testModif = testModification (off);
if (asUserException != null) {
if (Connection.DEBUG) System.err.println("already had an exception, will do nothing");
} else if (asUserLevel > 0 && ! testModif) {
if (Connection.DEBUG) System.err.println("Will queue up exception");
asUserException = new BadLocationException ("Cannot insert in a guard block", off);
} else {
if (! testModif) {
if (Connection.DEBUG) System.err.println("Inheriting guard blocks...");
MutableAttributeSet attr2 = (attr == null) ? new SimpleAttributeSet () : new SimpleAttributeSet (attr);
attr2.addAttribute (GUARDED, Boolean.TRUE);
attr2.addAttributes (GUARDED_LOOK);
attr = attr2;
}
super.insertString (off, str, attr);
}
} finally {
writeUnlock2 ();
}
}
public void remove (int off, int len) throws BadLocationException {
if (Connection.DEBUG) System.err.println("remove: off=" + off + " len=" + len);
try {
writeLock2 ();
if (asUserException != null) {
if (Connection.DEBUG) System.err.println("already had an exception, will do nothing");
} else if (asUserLevel > 0 && ! testModification (off)) {
if (Connection.DEBUG) System.err.println("Will queue up exception");
asUserException = new BadLocationException ("Cannot remove from a guard block", off);
} else {
super.remove (off, len);
}
} finally {
writeUnlock2 ();
}
}
private static final int CHUNK_MAX = 4096;
private void insertInChunks (int off, String text) throws BadLocationException {
int len = text.length ();
if (Connection.DEBUG) System.err.println("insertInChunks; len=" + len);
int pos = 0;
while (pos < len) {
int toSend = Math.min (len - pos, CHUNK_MAX);
if (Connection.DEBUG) System.err.println("insertInChunks: toSend=" + toSend);
Object[] result = proxy.function (FUN_insert,
new Object[] { new Integer (off + pos), text.substring (pos, pos + toSend) });
if (result.length > 0) {
if (result.length != 2) throw new RuntimeException ();
throw new BadLocationException ((String) result[0], ((Integer) result[1]).intValue ());
}
pos += toSend;
}
if (pos > len) throw new RuntimeException ("should not happen");
}
protected void insertUpdate (DefaultDocumentEvent ev, AttributeSet attr) {
super.insertUpdate (ev, attr);
if (Connection.DEBUG) System.err.println("insertUpdate: " + ev.getPresentationName () + " attr=" + attr);
synchronized (nofireLock) {
if (! nofire) {
int off = ev.getOffset ();
int len = ev.getLength ();
if (proxy != null) {
try {
String text = ev.getDocument ().getText (off, len);
insertInChunks (off, text);
} catch (BadLocationException ble) {
ble.printStackTrace ();
// XXX could try to undo the edit??
markAsBogus ("Could not insert: " + ble);
return;
}
// XXX now these will not update char attrs for bg color...
// however this case probably does not happen in IDE normally,
// since usually text is inserted and then guarded later
if (isGuarded (attr)) proxy.call (CMD_guard, new Object[] { new Integer (off), new Integer (len) });
} else {
if (Connection.DEBUG) System.err.println("Skipping insert due to proxy == null");
if (isGuarded (attr)) addGuard (off, len, true);
}
} else {
if (Connection.DEBUG) System.err.println("Skipping insertUpdate due to nofire");
}
}
makeEditUndoable (ev);
}
protected void postRemoveUpdate (DefaultDocumentEvent ev) {
super.postRemoveUpdate (ev);
if (Connection.DEBUG) System.err.println("postRemoveUpdate: " + ev.getPresentationName ());
synchronized (nofireLock) {
if (! nofire) {
if (proxy == null) {
if (Connection.DEBUG) System.err.println("Skipping postRemoveUpdate since proxy==null");
return;
}
Object[] result = proxy.function (FUN_remove,
new Object[] { new Integer (ev.getOffset ()), new Integer (ev.getLength ()) });
if (result.length != 0) {
if (result.length != 2) throw new RuntimeException ();
// XXX could try to undo the edit??
markAsBogus ("Could not remove: " + (String) result[0] + " at position #" + (Integer) result[1]);
}
} else {
if (Connection.DEBUG) System.err.println("Skipping postRemoveUpdate due to nofire");
}
}
makeEditUndoable (ev);
}
// XXX check if UndoRedo.NONE works as well
private static final UndoableEdit CANNOT_UNDO = new UndoableEdit () {
public void undo () throws CannotUndoException {
throw new CannotUndoException ();
}
public boolean canUndo () {
return false;
}
public void redo () throws CannotRedoException {
throw new CannotRedoException ();
}
public boolean canRedo () {
return false;
}
public void die () {
}
public boolean addEdit (UndoableEdit ue2) {
return false;
}
public boolean replaceEdit (UndoableEdit ue2) {
return false;
}
public boolean isSignificant () {
return true;
}
public String getPresentationName () {
return "<Cannot be undone>";
}
public String getUndoPresentationName () {
return getPresentationName ();
}
public String getRedoPresentationName () {
return getPresentationName ();
}
};
protected void makeEditUndoable (UndoableEdit ue) {
ue.addEdit (CANNOT_UNDO);
}
public void setCharacterAttributes (int off, int len, AttributeSet attrs, boolean repl) {
super.setCharacterAttributes (off, len, attrs, repl);
if (Connection.DEBUG) System.err.println("setCharAttrs: off=" + off + " len=" + len + " attrs=" + attrs + " repl=" + repl);
updateAttrs (off, len, attrs, repl);
}
public void setParagraphAttributes (int off, int len, AttributeSet attrs, boolean repl) {
super.setParagraphAttributes (off, len, attrs, repl);
if (Connection.DEBUG) System.err.println("setParaAttrs: off=" + off + " len=" + len + " attrs=" + attrs + " repl=" + repl);
updateAttrs (off, len, attrs, repl);
}
protected void updateAttrs (final int off, final int len, AttributeSet attrs, boolean repl) {
if (isGuarded (attrs)) {
if (proxy == null)
addGuard (off, len, true);
else
proxy.call (CMD_guard, new Object[] { new Integer (off), new Integer (len) });
super.setCharacterAttributes (off, len, GUARDED_LOOK, false);
} else if (repl || isUnguarded (attrs)) {
// XXX with repl=true, will sometimes gratuitously unguard, but oh well...
if (proxy == null)
addGuard (off, len, false);
else
proxy.call (CMD_unguard, new Object[] { new Integer (off), new Integer (len) });
super.setCharacterAttributes (off, len, UNGUARDED_LOOK, false);
}
}
private static boolean isGuarded (AttributeSet attrs) {
return attrs != null && Boolean.TRUE.equals (attrs.getAttribute (GUARDED));
}
private static boolean isUnguarded (AttributeSet attrs) {
return attrs != null && Boolean.FALSE.equals (attrs.getAttribute (GUARDED));
}
public void setLogicalStyle (final int pos, final Style s) {
if (Connection.DEBUG) System.err.println("setLogicalStyle: pos=" + pos + " s=" + s.getName ());
// XXX works around DocumentLine bug whereby this is called within a notification
if (getCurrentWriter () == null)
super.setLogicalStyle (pos, s);
else
SwingUtilities.invokeLater (new Runnable () {
public void run () {
if (Connection.DEBUG) System.err.println("setLogicalStyle II");
setLogicalStyle0 (pos, s);
}
});
String name = (s == null) ? null : s.getName ();
String toAnnounce = null;
if (name == null || NbDocument.NORMAL_STYLE_NAME.equals (name))
toAnnounce = STYLE_NORMAL;
else if (NbDocument.BREAKPOINT_STYLE_NAME.equals (name))
toAnnounce = STYLE_BREAKPOINT;
else if (NbDocument.CURRENT_STYLE_NAME.equals (name))
toAnnounce = STYLE_CURRENT;
else if (NbDocument.ERROR_STYLE_NAME.equals (name))
toAnnounce = STYLE_ERROR;
else
toAnnounce = STYLE_NORMAL;
if (proxy != null)
proxy.call (CMD_setStyle, new Object[] { new Integer (pos), toAnnounce });
else
addStyle (pos, toAnnounce);
}
private void setLogicalStyle0 (int pos, Style s) {
super.setLogicalStyle (pos, s);
}
public void runAtomicAsUser (Runnable r) throws BadLocationException {
if (Connection.DEBUG) System.err.println("runAtomicAsUser (start)");
try {
writeLock2 ();
if (atomicLevel == 1 && proxy != null) proxy.call (CMD_startAtomic);
if (asUserLevel++ == 0 && proxy != null) proxy.call (CMD_setAsUser, new Object[] { Boolean.TRUE });
r.run ();
if (asUserLevel == 1 && asUserException != null) {
if (Connection.DEBUG) System.err.println("Will actually throw: " + asUserException);
BadLocationException temp = asUserException;
asUserException = null;
throw temp;
}
} finally {
if (--asUserLevel == 0 && proxy != null) proxy.call (CMD_setAsUser, new Object[] { Boolean.FALSE });
if (atomicLevel == 1 && proxy != null) proxy.call (CMD_endAtomic);
writeUnlock2 ();
if (Connection.DEBUG) System.err.println("runAtomicAsUser (end)");
}
}
public void runAtomic (Runnable r) {
if (Connection.DEBUG) System.err.println("runAtomic (start)");
try {
writeLock2 ();
if (atomicLevel == 1 && proxy != null) proxy.call (CMD_startAtomic);
r.run ();
} finally {
if (atomicLevel == 1 && proxy != null) proxy.call (CMD_endAtomic);
writeUnlock2 ();
if (Connection.DEBUG) System.err.println("runAtomic (end)");
}
}
private void maybeSetTitle (Object value) {
if (value instanceof String && value != null && proxy != null) {
// XXX better not to use full package name for a title, IMHO
String sval = (String) value;
int slash = sval.lastIndexOf ('/');
if (slash != -1 && slash != sval.length () - 1)
sval = sval.substring (slash + 1);
proxy.call (CMD_setTitle, new Object[] { sval });
}
}
private class HookProperties extends Hashtable {
public HookProperties (Dictionary dict) {
Enumeration keys = dict.keys ();
while (keys.hasMoreElements ()) {
Object key = keys.nextElement ();
put (key, dict.get (key));
}
}
public Object put (Object key, Object value) {
if (value != null) {
if (value.equals (super.get (key))) {
if (Connection.DEBUG) System.err.println("Ignoring unchanged doc prop: " + key + "=" + value);
return value;
}
if (key.equals (TitleProperty)) {
maybeSetTitle (value);
} else if (key.equals (PROP_locAndSize) && value instanceof Rectangle) {
Rectangle r = (Rectangle) value;
if (proxy != null)
proxy.call (CMD_setLocAndSize, new Object[] { new Integer (r.x), new Integer (r.y),
new Integer (r.width), new Integer (r.height) });
} else if (key.equals (StreamDescriptionProperty) && value instanceof DataObject) {
DataObject dob = (DataObject) value;
if (proxy != null) proxy.call (CMD_setModified, new Object[] { new Boolean (dob.isModified ()) });
dob.addPropertyChangeListener (WeakListener.propertyChange (dobPCL, dob));
}
}
if (Connection.DEBUG) System.err.println("Setting doc prop: " + key + "=" + value);
return super.put (key, value);
}
public Object get (Object key) {
Object res = super.get (key);
//if (Connection.DEBUG) System.err.println("Getting doc prop: " + key + "=" + res);
return res;
}
}
}